Skip to content

fix(types): resolve remaining mypy errors missed by #1253#1296

Merged
kovtcharov-amd merged 10 commits into
mainfrom
fix/mypy-remaining
Jun 1, 2026
Merged

fix(types): resolve remaining mypy errors missed by #1253#1296
kovtcharov-amd merged 10 commits into
mainfrom
fix/mypy-remaining

Conversation

@kovtcharov-amd
Copy link
Copy Markdown
Collaborator

PR #1253 fixed the first batch of mypy errors but missed 12 more that surface when the full codebase is linted. These are blocking CI on all open PRs (#1243, #1244, #1245, #1248, #1249). Once this merges and the open PRs rebase, their lint checks will pass.

Test plan

  • python util/lint.py --all passes with 0 mypy errors

Suppresses no-any-return in providers and factory, fixes Optional
defaults in database.py, adds type: ignore for file_watcher redefs,
casts TransitionStatus in checkpoint_bridge.
@kovtcharov-amd kovtcharov-amd added bug Something isn't working tech debt labels May 29, 2026
@kovtcharov-amd kovtcharov-amd self-assigned this May 29, 2026
@github-actions github-actions Bot added llm LLM backend changes performance Performance-critical changes agents labels May 29, 2026
Ovtcharov added 2 commits May 29, 2026 10:27
…faults

- checkpoint_bridge.py: add `from typing import cast` and CheckpointStatus import
- factory.py: suppress no-any-return on dynamic provider instantiation
- claude.py: fix vision() to materialize Iterator[str] before returning
- database.py: fix update_session Optional defaults, create_session return type
PathValidator.create_backup() creates timestamped backups like
file.YYYYMMDD_HHMMSS.bak.py, not file.py.bak. The assertion was
checking the wrong path, causing a flaky failure.
@github-actions github-actions Bot added the tests Test changes label May 29, 2026
Ovtcharov added 2 commits May 29, 2026 10:50
…up test

- database.py: fix all Optional defaults in add_message/update_session,
  fix return types for add_document/add_message, type params list as Any
- test_code_agent.py: fix second backup assertion in integration test
- database/mixin.py: add assert narrowing after _require_db()
- device.py: cast registry value to str
- installer/mcp_init.py: annotate config_data dict
- rag/pdf_utils.py: annotate positions list
- testing/mocks.py: wrap return in dict()
- ui/database.py: fix Optional defaults in update_document_status, get_setting
- test_code_agent.py: fix second backup assertion in integration test
@github-actions github-actions Bot added the rag RAG system changes label May 29, 2026
Ovtcharov added 3 commits May 29, 2026 11:40
- connectors/store.py: suppress no-any-return on sqlite row returns
- api/app.py: use set comprehension for pids (was list assigned to set)
- validation_parsing.py: suppress attr-defined for mixin attributes
- checklist_generator.py: suppress no-any-return on dict.get returns
- session.py: suppress dict-item on error return branch
- tools.py: annotate _TOOL_REGISTRY, fix Optional func parameter
…l defaults

- tools.py: suppress no-any-return and return-value on get_tool_description
- memory_store.py: bulk fix str = None -> str | None = None across all methods
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 1, 2026

The type-annotation cleanup work here is correct and systematic — the T = NoneT | None = None sweep, assert-narrowing in DatabaseMixin, and cast() in python_factory.py all follow the right patterns. Three editing accidents landed in the diff, however, and two of them affect LLM provider files that ship to users.


Issues

🟡 Duplicate return statement — claude.py and openai_provider.py

Both LLM provider files now contain two back-to-back identical return lines. The second line is unreachable dead code — the edit changed the # type: ignore tag on the original line but also inserted a new copy instead of replacing it.

src/gaia/llm/providers/claude.py:77-78

        return response.content[0].text  # type: ignore[no-any-return]

src/gaia/llm/providers/openai_provider.py:63-64

        return response.choices[0].message.content  # type: ignore[no-any-return]

Neither causes a runtime failure (the second return is never reached), but --warn-unreachable will flag both, and mypy 1.9+ treats unreachable code as an error in strict mode — which defeats the goal of this PR.


🟡 Duplicate import — checkpoint_bridge.py:13-15

from typing import cast was added at line 13 and already existed at line 15 (the original). The duplicate survives isort but will trigger F811 (redefinition of unused name) from flake8/ruff.

from threading import Lock
from typing import cast

🟢 Two metadata: dict = None parameters left un-fixed — memory_store.py:678, 1226

store_knowledge() and update() both still have metadata: dict = None while every other = None parameter in those signatures was corrected. Mypy with --no-implicit-optional will flag these the same way it flagged the others.

        metadata: dict | None = None,

Applies at both src/gaia/agents/base/memory_store.py:678 and :1226.


Strengths

  • The assert self._db is not None narrowing in database/mixin.py is the right idiom here — it tells mypy that _require_db() guarantees the invariant without requiring a cast or an inline if on every caller.
  • Converting pids to a set comprehension in api/app.py:345 is a quiet improvement: it deduplicates PIDs before the kill loop with no change to the calling contract.
  • The str(name).strip() fix in device.py:440 and int(row["cnt"]) coercions in ui/database.py are correct at the type boundary (registry returns Any; SQLite row counts are int by contract).

Verdict

Request changes — the two duplicate return lines in the LLM providers need to be removed before merge. The duplicate import in checkpoint_bridge.py and the two un-fixed metadata parameters are straightforward clean-ups that should go in the same commit. All four fixes are one-line deletions or substitutions.

@kovtcharov-amd kovtcharov-amd requested a review from itomek-amd June 1, 2026 17:15
# Conflicts:
#	src/gaia/governance/checkpoint_bridge.py
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 1, 2026

One critical edit error slipped into an otherwise clean mypy sweep: both LLM provider files now contain duplicate back-to-back return statements, producing unreachable dead code. Runtime behaviour is unchanged (the first return fires), but this may trigger a new [unreachable] mypy error on CI — the opposite of what this PR intends.


Issues

🔴 Duplicate return statements in both LLM providers (claude.py:77-78, openai_provider.py:63-64)

Both files have an identical line duplicated instead of replaced. The second return is unreachable:

# claude.py lines 77-78
return response.content[0].text  # type: ignore[no-any-return]
return response.content[0].text  # type: ignore[no-any-return]   ← dead code

Python silently ignores the second line at runtime, but mypy may flag it as [unreachable], which would break the CI lint gate this PR is trying to fix. Fix by removing the duplicate.

src/gaia/llm/providers/claude.py:77-78 — drop one line:

        return response.content[0].text  # type: ignore[no-any-return]

Same applies to src/gaia/llm/providers/openai_provider.py:63-64:

        return response.choices[0].message.content  # type: ignore[no-any-return]

🟡 cursor.lastrowid or 0 conflates "no ROWID" with rowid=0 (database/mixin.py:403, ui/database.py:621)

cursor.lastrowid is None only for WITHOUT ROWID tables — which this codebase doesn't use. On any normal ROWID table the value is always an int, so or 0 is safe in practice. However, 0 is a valid rowid in SQLite, meaning callers can't distinguish the two cases if a WITHOUT ROWID table ever appears. Low risk given the schema, but a comment would make the intent explicit:

        return cursor.lastrowid or 0  # only None for WITHOUT ROWID tables (not used here)

🟢 dict(calls[call_index]["args"]) silently changes return semantics (testing/mocks.py:483)

The original returned a direct reference to the stored args dict; the new version returns a shallow copy. This is safer for test isolation (mutations don't affect the mock's internal state), but it's a subtle behavioural change that could surprise tests relying on object identity. Low risk — worth a one-line comment if anything in the test suite mutates the result.


🟢 # type: ignore suppressions across several modules

connectors/store.py, utils/parsing.py, checklist_generator.py, validation_parsing.py, and agents/chat/session.py now suppress no-any-return / dict-item rather than fixing the underlying type. This is acceptable as a targeted unblock when fixing the callees would require a larger refactor, but these should be revisited when those functions are properly typed.


Strengths

  • The | None annotation sweep across memory_store.py and ui/database.py (30+ parameters) is exactly the right Python 3.10+ pattern and is applied consistently.
  • assert self._db is not None after _require_db() is the canonical way to narrow an Optional attribute for mypy without runtime cost — correct given that _require_db() already raises RuntimeError on None.
  • cast(TransitionStatus, ...) in checkpoint_bridge.py and cast(Dict[str, Any], ...) in python_factory.py are cleaner choices than type: ignore where the type is statically knowable.
  • The set comprehension in api/app.py is a clean two-for-one: fewer lines and implicit PID deduplication.

Verdict

Request changes — the duplicate return in claude.py and openai_provider.py must be removed before merge. Both are single-line deletions; everything else is either clean or minor.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 1, 2026

Correct, well-scoped fix that unblocks CI for five open PRs. Almost all changes are straightforward X = NoneX | None = None modernizations or narrow # type: ignore comments; a handful deserve a closer look.


Issues

🟡 get_tool_display_label suppresses instead of fixing (tools.py:115–124)

The function is declared -> str but can return None (when tool.get("display_label") produces nothing). The PR silences this with # type: ignore[return-value] rather than correcting the annotation. The right fix is a one-liner — and matters because callers expecting str may not guard against None:

def get_tool_display_label(tool_name: str) -> str | None:

The two # type: ignore[return-value] comments in the body can then be removed.


🟢 Two metadata: dict = None parameters left unfixed (memory_store.py:678, memory_store.py:1226)

store_knowledge() and update() each have a metadata: dict = None parameter that was not converted to dict | None = None. These are inconsistent with the 20+ other parameters in the same file that were fixed. They don't appear to be causing the current CI failures, but they should be cleaned up for consistency.

        metadata: dict | None = None,

Apply at both memory_store.py:678 and memory_store.py:1226.


🟢 api/app.py:200 — list→set is a consistency win, not just a type fix

The Windows branch already used pids = set() (line 163), so converting the Linux/Mac branch to a set comprehension makes both branches consistent. Calling this out because the PR description frames all changes as type-only — this one is also a minor logical improvement (deduplicates PIDs, matches Windows path). No action needed; just worth noting for reviewers auditing stop_server behavior.


🟢 assert self._db is not None in production paths (database/mixin.py:388, 396, 412, 420, 428)

assert statements are stripped by Python's optimizer when running with -O, so relying on them for runtime narrowing in a library is fragile. Since _require_db() already raises before these points, mypy just needs to be told the value is non-None. A cast is safer:

        self._require_db()
        db = cast(sqlite3.Connection, self._db)
        cursor = db.execute(sql, params or {})

(repeat pattern for each method). This is a minor style concern — the asserts work fine in practice since -O is rarely used in GAIA's deployment paths.


Strengths

  • The T | None sweep across 19 files is clean and complete — no missed cases except the two metadata: dict parameters above.
  • Using cast(TransitionStatus, outcome_status) in checkpoint_bridge.py is the right tool here rather than a # type: ignore.
  • return cursor.lastrowid or 0 is safe: SQLite ROWIDs start at 1, so 0 is an unambiguous sentinel and keeps the return type int without breaking callers that test if msg_id.
  • The api/app.py set comprehension aligns the Linux path with the Windows path — a genuine consistency improvement bundled neatly with the type fix.

Verdict

Approve with suggestions. The CI-blocking errors are correctly resolved and no regressions are introduced. The get_tool_display_label return type annotation is the one item worth fixing before merge (it's a one-line change); the rest can be follow-up.

@kovtcharov-amd kovtcharov-amd enabled auto-merge June 1, 2026 19:21
@kovtcharov-amd kovtcharov-amd added this pull request to the merge queue Jun 1, 2026
Merged via the queue into main with commit bb9f43f Jun 1, 2026
40 checks passed
@kovtcharov-amd kovtcharov-amd deleted the fix/mypy-remaining branch June 1, 2026 19:24
@kovtcharov-amd kovtcharov-amd mentioned this pull request Jun 1, 2026
6 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents bug Something isn't working llm LLM backend changes performance Performance-critical changes rag RAG system changes tech debt tests Test changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants